package entity;

import handlers.RiverHandler;
import handlers.TeleportHandler;
import inventory.EquipLocation;
import inventory.EquippedInventory;
import inventory.Inventory;
import item.EquipableItem;
import item.InteractiveItem;
import item.Mount;
import item.NonWeapon;
import item.Obstacle;
import item.OneHanded;
import item.OneShotItem;
import item.Ranged;
import item.Requirements;
import item.Staff;
import item.TakeableItem;
import item.Trap;
import item.TwoHanded;
import item.UsableItem;
import loadsave.Memento;
import loadsave.loadsaveobject.Iterator;
import loadsave.loadsaveobject.LoadSaveObject;
import loadsave.loadsaveobject.LoadSaveObjectComposite;
import loadsave.loadsaveobject.LoadSavePrimitive;
import map.Direction;
import map.GameMap;
import map.Position;
import model.ModelObject;
import model.Subject;
import occupation.Occupation;
import stat.StatList;
import stat.StatType;
import visitors.EffectsVisitor;
import visitors.ItemVisitor;
import visitors.TakeableItemVisitor;
import effects.CombatEffect;
import effects.DamageEffect;
import effects.GainExperienceEffect;
import effects.HealEffect;
import effects.InstantDeathEffect;
import effects.RiverEffect;
import effects.TeleportEffect;

public abstract class Entity extends ModelObject {
	
	protected EquippedInventory equippedInventory;
	protected Inventory inventory;
	protected Occupation occupation;
	protected Position position;
	protected StatList stats;
	protected UseVisitor useVisitor;
	protected EffectVisitor effectVisitor;
	
	private Direction mDirection;
	
	private Subject deathSubject = new Subject("DEATH") { };
	
	public Entity() {
		super("");
		useVisitor = new UseVisitor();
		effectVisitor = new EffectVisitor();
	}
	public Entity(String modelName, Occupation occ) {
		super(modelName);
		inventory = new Inventory();
		equippedInventory = new EquippedInventory();
		useVisitor = new UseVisitor();
		effectVisitor = new EffectVisitor();
		occupation = occ;
		stats = occ.getInitStatList();
	}
	
	
	public abstract boolean checkReqs(Requirements req);
	
	public boolean move(Direction direction) {
		position.update(direction);
		mDirection = direction;
		notifyObservers();
		return true;
	}
	
	public void setName( String name ) {
		super.setName( name );
	}
	
	public void setPosition(Position position) {
		this.position = position;
		notifyObservers();
	}
	
	public InteractVisitor createIntVis(GameMap gameMap) {
		return new InteractVisitor(gameMap);
	}
	
	public EquippedInventory getEquippedInventory() {
		return equippedInventory;
	}
	
	public Position getPosition() {
		return position;
	}
	
	public Occupation getOccupation() {
		return occupation;
	}
	
	public void setOccupation(Occupation occ) {
		occupation = occ;
	}
	
	public Inventory getInventory() {
		return  inventory;
	}
	
	public EffectVisitor getEffectVisitor() {
		return effectVisitor;
	}
	
	public UseVisitor getUseVisitor() {
		return useVisitor;
	}
	
	public Direction getDirection() {
		return (mDirection);
	}
	
	public StatList getStats() {
		return stats;
	}
	
	public void setDirection(Direction dir) {
		this.mDirection = dir;
		notifyObservers();
	}
	/**
	 * Entitie's TakeableItemVisitor inner class. This is the class in charge
	 * of using items from the inventory and switching equipable items into the
	 * EquippedInventory
	 **/
	public class UseVisitor implements TakeableItemVisitor {

		public UseVisitor() {}
		
		@Override
		public void visitEquipableItem(EquipableItem equipableItem) {
			//Has no use for now
		}

		@Override
		public void visitUsableItem(UsableItem usableItem) {
			usableItem.getEffect().accept(Entity.this.new EffectVisitor());
			inventory.removeItem(usableItem);
		}

		@Override
		/**
		 * It adds the equipableItem to the EquippedInventory. If there is a weapon on that
		 * location it will switch it with the current one and place the replaced weapon in the
		 * regular inventory.
		 */
		public void visitOneHanded(OneHanded oneHanded) {
			EquipableItem item = equippedInventory.addItem(EquipLocation.WEAPON, oneHanded);
			inventory.removeItem(oneHanded);
			inventory.addItem(item);
			occupation.setActiveWeapon(oneHanded.getActiveSkillName());
		}

		@Override
		public void visitTwoHanded(TwoHanded twoHanded) {
			EquipableItem item = equippedInventory.addItem(EquipLocation.WEAPON, twoHanded);
			inventory.removeItem(twoHanded);
			inventory.addItem(item);
			occupation.setActiveWeapon(twoHanded.getActiveSkillName());
		}

		@Override
		public void visitRanged(Ranged ranged) {
			EquipableItem item = equippedInventory.addItem(EquipLocation.WEAPON, ranged);
			inventory.removeItem(ranged);
			inventory.addItem(item);
			occupation.setActiveWeapon(ranged.getActiveSkillName());
		}

		@Override
		public void visitStaff(Staff staff) {
			EquipableItem item = equippedInventory.addItem(EquipLocation.WEAPON, staff);
			inventory.removeItem(staff);
			inventory.addItem(item);
			occupation.setActiveWeapon(staff.getActiveSkillName());
		}

		@Override
		public void visitNonWeapon(NonWeapon nonWeapon) {
			EquipableItem item = equippedInventory.addItem(nonWeapon.getEquipLocation(), nonWeapon);
			inventory.removeItem(nonWeapon);
			inventory.addItem(item);
			occupation.setActiveWeapon(nonWeapon.getActiveSkillName());
			
		}

		@Override
		public void visitMount(Mount mount) {
			EquipableItem item = equippedInventory.addItem(EquipLocation.MOUNT, mount);
			inventory.removeItem(mount);
			
			int i = getName().indexOf('_');
			if( i != -1 ) {
				setName( getName().substring( 0, i ) );
			}
			
			inventory.addItem(item);
			setName( getName() + "_" + mount.getName() );
		}

	}
	/**
	 * Entitie's ItemVisitor inner class. This is the class in charge
	 * of using items from the map and putting them on the inventory or
	 * use them if they are usable
	 **/
	public class InteractVisitor implements ItemVisitor {
		private GameMap gameMap;
		
		public InteractVisitor(GameMap map) {
			gameMap = map;
		}
		
		public void setMap (GameMap map) {
			gameMap = map;
		}
		
		@Override
		public void visitInteractive(InteractiveItem interactiveItem) {
			if(checkReqs(interactiveItem.getRequirements())) {
				//change item's state
			}
			
		}
		@Override
		public void visitObstacle(Obstacle obstacleItem) {
			
			
		}
		@Override
		public void visitOneShotItem(OneShotItem oneshotItem) {
			oneshotItem.getEffect().accept(Entity.this.new EffectVisitor());
			gameMap.removeItem(position);
		}
		@Override
		public void visitTakeableItem(TakeableItem takeableItem) {
			inventory.addItem(takeableItem);
			gameMap.removeItem(position);
		}
		@Override
		public void visitTrap(Trap trapItem) {
			System.out.println("VisitTrap");
			trapItem.getEffect().accept(Entity.this.new EffectVisitor());
		}

		/**
		 * Automatically equip picked up mount
		 */
		public void visitMount(Mount mount) {
			EquipableItem item = equippedInventory.addItem(EquipLocation.MOUNT, mount);
			
			int i = getName().indexOf('_');
			if( i != -1 ) {
				setName( getName().substring( 0, i ) );
			}
			
			inventory.addItem(item);
			setName( getName() + "_" + mount.getName() );
			
			gameMap.removeItem(position);
		}
	}
	/**
	 * Entitie's EffectVisitor inner class. This is the class in charge
	 * of using effect
	 **/
	public class EffectVisitor implements EffectsVisitor {
		
		@Override
		public void visitCombatEffect(CombatEffect ce) {
			StatList ce_stats = ce.computeStats();
			
			for( StatType st : ce.getOutputStats() ) {
				ce_stats.addStat( st, statDifferential( ce_stats.getStat( st ) ) );
				
				System.out.println( "Modifying " + st.toString() + " by " + ce_stats.getStat( st ) * ce.getMutator() );
				
				stats.addStat( st, ce_stats.getStat( st ) * ce.getMutator() );
			}
			checkForDeath();
		}
		
		private int statDifferential( int statValue ) {
			return (statValue - stats.getStat(StatType.ARMORRATING)/4 - stats.getStat(StatType.DEFENSIVERATING)/4);
		}

		@Override
		public void visitDamageEffect(DamageEffect de) {
			stats.subtractStats(de.computeStats());
			checkForDeath();
		}

		@Override
		public void visitGainExperienceEffect(GainExperienceEffect gee) {
			stats.addStats(gee.computeStats());
		}

		@Override
		public void visitHealEffect(HealEffect he) {
			stats.addStats(he.computeStats());
		}

		@Override
		public void visitInstantDeathEffect(InstantDeathEffect ide) {
		    stats.subtractStats(ide.computeStats());
		    checkForDeath();
		}

		@Override
		public void visitTeleportEffect(TeleportEffect te) {
			
			TeleportHandler tph = new TeleportHandler(te.getMapName(), Entity.this);
			tph.teleport();
		}

		@Override
		public void visitRiverEffect(RiverEffect re) {
			
			RiverHandler rh = new RiverHandler(Entity.this, re.getDirection());
			rh.push();
		}
		
	}
	
	@Override
	public Memento getMemento() {
		return new Memento() {

			@Override
			public LoadSaveObject getState() {
				LoadSaveObjectComposite toReturn = new LoadSaveObjectComposite("", "Entity");
				Iterator<LoadSaveObject> i = Entity.super.getMemento().getState().getIterator();
				for (i.init(); i.hasNext(); i.next()) {
					toReturn.add(i.getCurrKey().getValue(), i.getCurrVal());
				}
				LoadSaveObject lso = equippedInventory.getMemento().getState();
				lso.setName("equippedInventory");
				toReturn.add(lso.getName(), lso);
				lso = inventory.getMemento().getState();
				lso.setName("inventory");
				toReturn.add(lso.getName(), lso);
				lso = occupation.getMemento().getState();
				lso.setName("occupation");
				toReturn.add(lso.getName(), lso);
				lso = position.getMemento().getState();
				lso.setName("position");
				toReturn.add(lso.getName(), lso);
				toReturn.add("mDirection", new LoadSavePrimitive("mDirection", "Direction", mDirection.toString()));
				lso = stats.getMemento().getState();
				lso.setName("stats");
				toReturn.add(lso.getName(), lso);
				return toReturn;
			}

			@Override
			public void restoreState(LoadSaveObject lso) {
				equippedInventory.getMemento().restoreState(lso.get("equippedInventory"));
				inventory.getMemento().restoreState(lso.get("inventory"));
				occupation.getMemento().restoreState(lso.get("occupation"));
				position.getMemento().restoreState(lso.get("position"));
				stats.getMemento().restoreState(lso.get("stats"));
				mDirection = Direction.valueOf(lso.get("mDirection").getValue());
			}
			
		};
	}
	
	public Subject getDeathSubject() {
		return deathSubject;
	}
	
	private void checkForDeath() {
		System.err.println("Checking for death...");
		System.err.println(getName());
		System.err.println(stats.toString());
		if (stats.getStat(StatType.LIVES) <= 0)
			deathSubject.notifyObservers();
	}
	
}